Skip to content

Add method coverage support#987

Closed
tycooon wants to merge 42 commits intosimplecov-ruby:mainfrom
umbrellio:add-method-coverage-support
Closed

Add method coverage support#987
tycooon wants to merge 42 commits intosimplecov-ruby:mainfrom
umbrellio:add-method-coverage-support

Conversation

@tycooon
Copy link
Copy Markdown

@tycooon tycooon commented Apr 23, 2021

See #782.
Also addresses #801 and #916.
Support for HTML formatter can be found here: simplecov-ruby/simplecov-html#110.

Some notes:

  • I had to rework how coverage is parsed/unparsed (see ResultSerialization module).
  • Because of that, I had to revert some of ResultMerger optimizations :( Now it's operating Result objects, however it still parses and merges files one be one.
  • Coverage is now always stored in memory in symbolized form (lines: [] instead of "lines" => []), just the same way you get it when calling Ruby's Coverage.result. Because of that, a lot of specs were updated.
  • Sometimes method owner includes memory address (e.g. when method is called on anonymous class). This makes merging results from different CI jobs problematic, so we replace memory addresses with zero address 0x0000000000000000.
  • adapt_pre_simplecov_0_18_result logic was moved to ResultSerialization.deserialize. Also old-style branch info is supported (eval is still used for that).

All this stuff has been battle-tested on a couple of big projects, some of which merge results of multiple CI jobs.

@tycooon tycooon force-pushed the add-method-coverage-support branch 2 times, most recently from 9851ba0 to 5987457 Compare April 24, 2021 11:27
@tycooon
Copy link
Copy Markdown
Author

tycooon commented Apr 24, 2021

@PragTob hope you will find some time to review the changes.

@tycooon tycooon force-pushed the add-method-coverage-support branch from 5987457 to 9a8b324 Compare April 24, 2021 13:06
@pboling
Copy link
Copy Markdown

pboling commented Mar 29, 2023

@PragTob ping!

1 similar comment
@0exp
Copy link
Copy Markdown

0exp commented Dec 20, 2023

@PragTob ping!

@tycooon tycooon force-pushed the add-method-coverage-support branch from 2ce910b to 54684e8 Compare December 25, 2023 16:13
@tycooon tycooon force-pushed the add-method-coverage-support branch from 54684e8 to 8ede952 Compare December 25, 2023 16:28
@sferik sferik force-pushed the main branch 9 times, most recently from 33cbb8e to 574b45c Compare December 26, 2023 16:15
Copy link
Copy Markdown

@gondalez gondalez Jan 4, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your work on this PR, it would make a welcome inclusion 🙏

I am curious - would method coverage improve coverage support for ruby 3's endless methods?

I have found that endless methods currently show as covered if they are never executed by tests.
Even if you have branching coverage enabled they report as fully covered.

Example class, spec and coverage report follow.
Note that uncovered_endless_method shows as covered despite not being executed by the test.
Whereas endless_branching_method shows branching coverage failing since it was executed.

I would have expected uncovered_endless_method to show as partially covered.

class CoveragePoc
  # this method is fully covered
  def self.simple_method
    :simple
  end

  # the :b branch for these methods is not covered
  def self.endless_branching_method(arg) = arg ? :a : :b

  def self.branching_method(arg)
    arg ? :a : :b
  end

  # these methods are not covered at all
  def self.uncovered_endless_method = :uncovered_endless

  def self.uncovered_method
    :uncovered
  end
end
RSpec.describe CoveragePoc do
  it 'works' do
    expect(described_class.simple_method).to eq(:simple)
    expect(described_class.branching_method(true)).to eq(:a)
    expect(described_class.endless_branching_method(true)).to eq(:a)
  end
end

CleanShot 2024-01-04 at 13 28 26@2x

@tycooon
Copy link
Copy Markdown
Author

tycooon commented Nov 3, 2025

Hey @amatsuda, maybe you could take a look? I know there are conflicts, but I don't want to fix them in case there are no plans to merge this PR anytime soon.

@amatsuda
Copy link
Copy Markdown
Member

amatsuda commented Nov 3, 2025

@tycooon Thank you for working on this! Yes, I'm very much interested in this feature, and willing to take a look once the patch is cleanly rebased against the main branch :)

@tycooon tycooon force-pushed the add-method-coverage-support branch from c2e0130 to c59387a Compare November 4, 2025 10:18
@tycooon
Copy link
Copy Markdown
Author

tycooon commented Nov 4, 2025

Hey @amatsuda, I just updated the branch 👍

end

def adapt_old_style_branch_info(value)
eval(value.to_s) # rubocop:disable Security/Eval
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this eval was in the original code, but I’d love to remove it.

Copy link
Copy Markdown
Collaborator

@sferik sferik left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@tycooon thanks for all your work on this PR! It's a great feature idea. I reviewed the code and left some minor feedback.

Reviewing this inspired me to take a crack at an alternative implementation that builds on the same ideas but makes a few different tradeoffs in add-method-coverage-support.

Here are main differences between my approach and yours:

  1. Keeps string keys ("lines", "branches", "methods") instead of converting to symbols. This avoids the breaking change for downstream consumers like rubycritic that access coverage_data directly, and avoids touching nearly every spec file.
  2. ResultMerger is completely untouched. The memory optimization of deferring Result object creation until the final merge step is preserved. Method data flows through the existing combine pipeline naturally since FilesCombiner/MethodsCombiner operate on raw hashes just like LinesCombiner/BranchesCombiner do.
  3. Replaces eval with a safe StringScanner-based parser in restore_ruby_data_structure. This addresses #801 without requiring a separate ResultSerialization class. The same parser handles both branch and method data keys after JSON round-tripping.
  4. Memory address normalization happens in ResultAdapter (early, before the JSON round-trip in Result.new) rather than during serialization.

Everything else is the same in spirit: the SourceFile::Method class, MethodsCombiner, configuration via enable_coverage :method, FileList aggregation, etc.

What do you think?


def total_methods
@total_methods ||= covered_methods + missed_methods
end
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The total_methods method defined in FileList returns a count, as the name suggests, but this one returns an array. Please change the name of this one (or change its behavior to be consistent with the other).

end

describe "method coverage" do
it "has total methods count 0" do
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
it "has total methods count 0" do
it "has total methods count 3" do

expect(subject.total_methods.size).to eq(3)
end

it "has covered methods count 0" do
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
it "has covered methods count 0" do
it "has covered methods count 2" do

expect(subject.covered_methods.size).to eq(2)
end

it "has missed methods count 0" do
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
it "has missed methods count 0" do
it "has missed methods count 1" do

end

def to_s
"#{klass}##{method}"
Copy link
Copy Markdown
Collaborator

@sferik sferik Mar 27, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ruby’s Coverage.result uses different prefixes for instance methods vs class methods (e.g., #<Class:Foo> for singleton methods). It should probably use using . instead of # for class methods.

end

def lines_data
coverage_data.fetch(:lines)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it possible for the :lines key to be missing (e.g. if the user just requests method coverage data)? If so, this will crash.

arm64-darwin
x86_64-darwin
x86_64-linux-gnu
x86_64-linux-musl
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This shouldn’t be part of this PR.

@tycooon
Copy link
Copy Markdown
Author

tycooon commented Mar 28, 2026

Hey @sferik, I like your approach more than mine. I am closing this PR for now 👍

Also don't forget to add some support for HTML formatter. Example of what I did is here simplecov-ruby/simplecov-html#110.

And btw since you added your own parser, maybe we can address #916 as I did in the same branch?

@tycooon tycooon closed this Mar 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

7 participants